Lambdaを使わずStep FunctionsでNature Remo Cloud APIのデータを収集する
IoT事業部のやまたつです!
今日はAWS Lambdaを使わずにAWS Step FunctionsからNature RemoのCloud APIを叩いて、お部屋の温度、湿度、照度をAmazon CloudWatch metricsに収集したいと思います!
概要
Nature Remoとは
Nature株式会社からリリースされている製品です。アプリやスマートスピーカーから家電を操作できるようになります。
Nature Remo Cloud APIとは
Nature Remoを操作したり、Nature Remoに内蔵されているセンサーから得られる情報を取得したりできるWeb APIです。
今回はこれを使って温度、湿度、照度の情報を取得してみようと思います。
アウトプット
こんな感じでお部屋の温度と湿度の時系列データが確認できるようになります!
※ Temperature
をタイポしています!?
AWS Step Functionsに何をさせるのか
- Parameter StoreからAPIのtokenを取ってくる
- Amazon API Gatewayを介してNature Remo Cloud APIからデータを取得する
- Amazon CloudWatch metricsにPutする。
AWS Lambdaでやれば良いんじゃないの?
AWS Step Functionsでやってみたかった。それ以上の回答を、僕は持ち合わせていないですね。
やっていく!
aws-cdkで書いていきます!v2を使います!
Parameter StoreからAPIのtokenを取ってくる
予め、Nature Remo Cloud APIのtokenをParameter Storeに入れておく必要があります。
僕は /plantor/nature-remo-token
という名前で入れました!
aws ssm put-parameter --name '/plantor/nature-remo-token' --value 'YOUR-TOKEN' --type 'String' --data-type 'text'
cdkはこんな感じ。
import { App, Stack, StackProps, aws_stepfunctions as sfn, aws_stepfunctions_tasks as tasks, } from "aws-cdk-lib"; type Props = StackProps & {}; export class NatureRemo extends Stack { constructor(parent: App, id: string, props: Props) { super(parent, id, props); const taskToGetSecret = new tasks.CallAwsService(this, "GetSecretTask", { service: "ssm", action: "getParameter", parameters: { Name: "/plantor/nature-remo-token" }, iamResources: ["*"], iamAction: "ssm:GetParameter", resultSelector: { "Token.$": "$.Parameter.Value", }, resultPath: "$.SecretOutput", }); new sfn.StateMachine(this, "MyStateMachine", { definition: taskToGetSecret, }); } }
new tasks.CallAwsService()
でParameter Storeのデータを取得しています。これは今年の9月に発表されたAWS Step FunctionsのAWS SDK統合の機能を使うものです。
発表から一週間程度でaws-cdkに機能が追加されてます。活きが良い!
これによりSDKさえ対応していればAWS Step Functionsから扱うことができます。すごい!
注意⚠️
今回のようにAWS Step Functions内で秘匿情報を扱うのは、個人プロダクトだけにするのが良いと思われます。なぜならAWS Step FunctionsのStateに格納される情報はWebコンソールなどから確認することができてしまうからです。大人しくLambdaを書きましょう。
Amazon API Gatewayを介してNature Remo Cloud APIからデータを取得する
次にNature Remo Cloud APIを叩けるようにしていきます!
まずはAmazon API Gatewayを用意します。
/** * AWS Step Functions の Amazon API Gateway Integration は Authorization header が使えない。 * そのため、カスタムヘッダーに入れて、Amazon API Gateway側で request parameter mappingしてあげるワークアラウンドを実装している。 * @see https://docs.aws.amazon.com/step-functions/latest/dg/connect-api-gateway.html#connect-api-gateway-requests */ const xAuthorization = "method.request.header.x-Authorization"; const remoEndpoint = new aws_apigateway.RestApi( this, "NatureRemoEndpoint", { defaultIntegration: new aws_apigateway.HttpIntegration( "https://api.nature.global/1/devices", { options: { requestParameters: { "integration.request.header.Authorization": xAuthorization, }, }, }, ), }, ); remoEndpoint.root.addMethod("GET", undefined, { requestParameters: { [xAuthorization]: true }, });
そしてAWS Step Functionsにて使います!
const taskToCallApi = new tasks.CallApiGatewayRestApiEndpoint( this, "CallNatureRemoTask", { api: remoEndpoint, stageName: remoEndpoint.deploymentStage.stageName, method: tasks.HttpMethod.GET, headers: sfn.TaskInput.fromObject({ "x-Authorization": sfn.JsonPath.stringAt( /** * ドキュメントでは「Listでもいいよ」みたいに書いてあるけど、実際には配列じゃないと実行時エラーになる。 * なので `States.Array()` が必要。 * @see https://docs.aws.amazon.com/step-functions/latest/dg/connect-api-gateway.html */ "States.Array(States.Format('Bearer {}', $.SecretOutput.Token))", ), }), resultSelector: { "Events.$": "$.ResponseBody[1].newest_events", }, resultPath: "$.NatureRemoOutput", }, ); new sfn.StateMachine(this, "MyStateMachine", { definition: taskToGetSecret.next(taskToCallApi).next(taskToPutMetric), });
注意⚠️
コード中のコメントにもある通り、AWS Step FunctionsのAmazon API Gateway統合ではAuthorization headerを使うことは許可されていません。そのため上記コードではAuthorization headerを使わないワークアラウンドを実装しています。ご利用は自己責任でお願いします??♂️
Amazon CloudWatch metricsにPutする。
もう一息!もう一度、AWS Step FunctionsのSDK統合を使ってcloudwatch putMetricData
を呼び出します!
const taskToPutMetric = new tasks.CallAwsService(this, "PutMetricTask", { service: "cloudwatch", action: "putMetricData", parameters: { Namespace: "CUSTOM-IoT/Room", MetricData: [ { MetricName: "Temperature", Value: sfn.JsonPath.numberAt("$.NatureRemoOutput.Events.te.val"), }, { MetricName: "Illuminance", Value: sfn.JsonPath.numberAt("$.NatureRemoOutput.Events.il.val"), }, { MetricName: "Humidity", Value: sfn.JsonPath.numberAt("$.NatureRemoOutput.Events.hu.val"), }, ], }, iamResources: ["*"], iamAction: "cloudwatch:PutMetricData", resultPath: "$.PutMetricOutput", }); const stateMachine = new sfn.StateMachine(this, "MyStateMachine", { definition: taskToGetSecret.next(taskToCallApi).next(taskToPutMetric), });
完成!
コードの全体は以下のとおりです。
import { App, Duration, Stack, StackProps, aws_events, aws_events_targets, aws_stepfunctions as sfn, aws_stepfunctions_tasks as tasks, aws_apigateway, } from "aws-cdk-lib"; type Props = StackProps & {}; export class NatureRemo extends Stack { constructor(parent: App, id: string, props: Props) { super(parent, id, props); const remoEndpoint = this.natureRemoEndpoint(); const taskToGetSecret = new tasks.CallAwsService(this, "GetSecretTask", { service: "ssm", action: "getParameter", parameters: { Name: "/plantor/nature-remo-token" }, iamResources: ["*"], iamAction: "ssm:GetParameter", resultSelector: { "Token.$": "$.Parameter.Value", }, resultPath: "$.SecretOutput", }); const taskToCallApi = new tasks.CallApiGatewayRestApiEndpoint( this, "CallNatureRemoTask", { api: remoEndpoint, stageName: remoEndpoint.deploymentStage.stageName, method: tasks.HttpMethod.GET, headers: sfn.TaskInput.fromObject({ "x-Authorization": sfn.JsonPath.stringAt( /** * ドキュメントでは「Listでもいいよ」みたいに書いてあるけど、実際には配列じゃないと実行時エラーになる。 * なので `States.Array()` が必要。 * @see https://docs.aws.amazon.com/step-functions/latest/dg/connect-api-gateway.html */ "States.Array(States.Format('Bearer {}', $.SecretOutput.Token))", ), }), resultSelector: { "Events.$": "$.ResponseBody[1].newest_events", }, resultPath: "$.NatureRemoOutput", }, ); const taskToPutMetric = new tasks.CallAwsService(this, "PutMetricTask", { service: "cloudwatch", action: "putMetricData", parameters: { Namespace: "CUSTOM-IoT/Room", MetricData: [ { MetricName: "Temperature", Value: sfn.JsonPath.numberAt("$.NatureRemoOutput.Events.te.val"), }, { MetricName: "Illuminance", Value: sfn.JsonPath.numberAt("$.NatureRemoOutput.Events.il.val"), }, { MetricName: "Humidity", Value: sfn.JsonPath.numberAt("$.NatureRemoOutput.Events.hu.val"), }, ], }, iamResources: ["*"], iamAction: "cloudwatch:PutMetricData", resultPath: "$.PutMetricOutput", }); const stateMachine = new sfn.StateMachine(this, "MyStateMachine", { definition: taskToGetSecret.next(taskToCallApi).next(taskToPutMetric), }); new aws_events.Rule(this, "ScheduleRule", { schedule: aws_events.Schedule.rate(Duration.minutes(60)), targets: [new aws_events_targets.SfnStateMachine(stateMachine)], }); } /** * 後ろ側にnatureRemoのAPIを設定したAmazon API Gateway */ private natureRemoEndpoint() { /** * AWS Step Functions の Amazon API Gateway Integration は Authorization header が使えない。 * そのため、カスタムヘッダーに入れて、Amazon API Gateway側で request parameter mappingしてあげるワークアラウンドを実装している。 * @see https://docs.aws.amazon.com/step-functions/latest/dg/connect-api-gateway.html#connect-api-gateway-requests */ const xAuthorization = "method.request.header.x-Authorization"; const remoEndpoint = new aws_apigateway.RestApi( this, "NatureRemoEndpoint", { defaultIntegration: new aws_apigateway.HttpIntegration( "https://api.nature.global/1/devices", { options: { requestParameters: { "integration.request.header.Authorization": xAuthorization, }, }, }, ), }, ); remoEndpoint.root.addMethod("GET", undefined, { requestParameters: { [xAuthorization]: true }, }); return remoEndpoint; } }
今回作成したコードはGitHubにもコミットしてあります。
まとめ
AWS Lambdaのコードを書かずに外部APIの結果をメトリクスデータとして可視化できました!
「結局cdkのためにtypescript書いてるやないかい」というツッコミは甘んじて受けます!
AWS Lambdaのコードを書くかどうかは置いておいても、SDK統合によってAWS Step Functionsが格段にパワーアップしたのを感じました!
もし機会があればぜひ使ってみてください!
以上、やまたつでした!